/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

package org.apache.jmeter.testelement;

import java.io.Serializable;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.jmeter.testelement.property.BooleanProperty;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.MapProperty;
import org.apache.jmeter.testelement.property.MultiProperty;
import org.apache.jmeter.testelement.property.NullProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.testelement.property.PropertyIteratorImpl;
import org.apache.jmeter.testelement.property.StringProperty;
import org.apache.jmeter.testelement.property.TestElementProperty;
import org.apache.jmeter.threads.JMeterContext;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;

/**
 */
public abstract class AbstractTestElement implements TestElement, Serializable {
	private static final Logger log = LoggingManager.getLoggerForClass();

	private Map propMap = Collections.synchronizedMap(new LinkedHashMap());

	private transient Set temporaryProperties;

	private transient boolean runningVersion = false;

	// Thread-specific variables saved here to save recalculation
	private transient JMeterContext threadContext = null;

	private transient String threadName = null;

	public Object clone() {
		try {
			TestElement clonedElement = (TestElement) this.getClass().newInstance();

			PropertyIterator iter = propertyIterator();
			while (iter.hasNext()) {
				clonedElement.setProperty((JMeterProperty) iter.next().clone());
			}
			clonedElement.setRunningVersion(runningVersion);
			return clonedElement;
		} catch (InstantiationException e) {
			throw new AssertionError(e); // clone should never return null
        } catch (IllegalAccessException e) {
        	throw new AssertionError(e); // clone should never return null
        }
	}

	public void clear() {
		propMap.clear();
	}

	public void removeProperty(String key) {
		propMap.remove(key);
	}

	public boolean equals(Object o) {
		if (o instanceof AbstractTestElement) {
			return ((AbstractTestElement) o).propMap.equals(propMap);
		} else {
			return false;
		}
	}

	// TODO temporary hack to avoid unnecessary bug reports for subclasses
	
	public int hashCode(){
		return System.identityHashCode(this);
	}
	/*
	 * URGENT: TODO - sort out equals and hashCode() - at present equal
	 * instances can/will have different hashcodes - problem is, when a proper
	 * hashcode is used, tests stop working, e.g. listener data disappears when
	 * switching views... This presumably means that instances currently
	 * regarded as equal, aren't really equal...
	 * 
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	// This would be sensible, but does not work:
	// public int hashCode()
	// {
	// return propMap.hashCode();
	// }
	public void addTestElement(TestElement el) {
		mergeIn(el);
	}

	public void setName(String name) {
		setProperty(TestElement.NAME, name);
	}

	public String getName() {
		return getPropertyAsString(TestElement.NAME);
	}

	public void setComment(String comment){
		setProperty(new StringProperty(TestElement.COMMENTS, comment));
	}
	
	public String getComment(){
		return getProperty(TestElement.COMMENTS).getStringValue();
	}

	/**
	 * Get the named property. If it doesn't exist, a new NullProperty object is
	 * created with the same name and returned.
	 */
	public JMeterProperty getProperty(String key) {
		JMeterProperty prop = (JMeterProperty) propMap.get(key);
		if (prop == null) {
			prop = new NullProperty(key);
		}
		return prop;
	}

	public void traverse(TestElementTraverser traverser) {
		PropertyIterator iter = propertyIterator();
		traverser.startTestElement(this);
		while (iter.hasNext()) {
			traverseProperty(traverser, iter.next());
		}
		traverser.endTestElement(this);
	}

	protected void traverseProperty(TestElementTraverser traverser, JMeterProperty value) {
		traverser.startProperty(value);
		if (value instanceof TestElementProperty) {
			((TestElement) value.getObjectValue()).traverse(traverser);
		} else if (value instanceof CollectionProperty) {
			traverseCollection((CollectionProperty) value, traverser);
		} else if (value instanceof MapProperty) {
			traverseMap((MapProperty) value, traverser);
		}
		traverser.endProperty(value);
	}

	protected void traverseMap(MapProperty map, TestElementTraverser traverser) {
		PropertyIterator iter = map.valueIterator();
		while (iter.hasNext()) {
			traverseProperty(traverser, iter.next());
		}
	}

	protected void traverseCollection(CollectionProperty col, TestElementTraverser traverser) {
		PropertyIterator iter = col.iterator();
		while (iter.hasNext()) {
			traverseProperty(traverser, iter.next());
		}
	}

	public int getPropertyAsInt(String key) {
		return getProperty(key).getIntValue();
	}

	public boolean getPropertyAsBoolean(String key) {
		return getProperty(key).getBooleanValue();
	}

	public boolean getPropertyAsBoolean(String key, boolean defaultVal) {
		JMeterProperty jmp = getProperty(key);
		return jmp instanceof NullProperty ? defaultVal : jmp.getBooleanValue();
	}

	public float getPropertyAsFloat(String key) {
		return getProperty(key).getFloatValue();
	}

	public long getPropertyAsLong(String key) {
		return getProperty(key).getLongValue();
	}

	public double getPropertyAsDouble(String key) {
		return getProperty(key).getDoubleValue();
	}

	public String getPropertyAsString(String key) {
		return getProperty(key).getStringValue();
	}

    public String getPropertyAsString(String key, String defaultValue) {
        JMeterProperty jmp = getProperty(key);
        return jmp instanceof NullProperty ? defaultValue : jmp.getStringValue();
    }

	protected void addProperty(JMeterProperty property) {
		if (isRunningVersion()) {
			setTemporary(property);
		} else {
			clearTemporary(property);
		}
		JMeterProperty prop = getProperty(property.getName());

		if (prop instanceof NullProperty || (prop instanceof StringProperty && prop.getStringValue().equals(""))) {
			propMap.put(property.getName(), property);
		} else {
			prop.mergeIn(property);
		}
	}

	protected void clearTemporary(JMeterProperty property) {
		if (temporaryProperties != null) {
			temporaryProperties.remove(property);
		}
	}

	/**
	 * Log the properties of the test element
	 * 
	 * @see TestElement#setProperty(JMeterProperty)
	 */
	protected void logProperties() {
		if (log.isDebugEnabled()) {
			PropertyIterator iter = propertyIterator();
			while (iter.hasNext()) {
				JMeterProperty prop = iter.next();
				log.debug("Property " + prop.getName() + " is temp? " + isTemporary(prop) + " and is a "
						+ prop.getObjectValue());
			}
		}
	}

	public void setProperty(JMeterProperty property) {
		if (isRunningVersion()) {
			if (getProperty(property.getName()) instanceof NullProperty) {
				addProperty(property);
			} else {
				getProperty(property.getName()).setObjectValue(property.getObjectValue());
			}
		} else {
			propMap.put(property.getName(), property);
		}
	}

	public void setProperty(String name, String value) {
		setProperty(new StringProperty(name, value));
	}

// TODO - enable this for the next version
// Doing so now will generate huge Javadoc changes...

//    /**
//     * Create a String property - but only if it is not the default.
//     * This is intended for use when adding new properties to JMeter
//     * so that JMX files are not expanded unnecessarily.
//     * 
//     * N.B. - must agree with the default applied when reading the property.
//     * 
//     * @param name property name
//     * @param value current value
//     * @param dflt default
//     */
//    public void setProperty(String name, String value, String dflt) {
//        if (dflt.equals(value)) {
//            removeProperty(name);
//        } else {
//            setProperty(new StringProperty(name, value));
//        }
//    }

	public void setProperty(String name, boolean value) {
		setProperty(new StringProperty(name, Boolean.toString(value)));
	}

	/**
	 * Create a boolean property - but only if it is not the default.
	 * This is intended for use when adding new properties to JMeter
	 * so that JMX files are not expanded unnecessarily.
	 * 
	 * N.B. - must agree with the default applied when reading the property.
	 * 
	 * @param name property name
	 * @param value current value
	 * @param dflt default
	 */
	public void setProperty(String name, boolean value, boolean dflt) {
		if (value == dflt) {
			removeProperty(name);
		} else {
			setProperty(new BooleanProperty(name, value));
		}
	}
	
	public PropertyIterator propertyIterator() {
		return new PropertyIteratorImpl(propMap.values());
	}

	protected void mergeIn(TestElement element) {
		PropertyIterator iter = element.propertyIterator();
		while (iter.hasNext()) {
			JMeterProperty prop = iter.next();
			addProperty(prop);
		}
	}

	/**
	 * Returns the runningVersion.
	 */
	public boolean isRunningVersion() {
		return runningVersion;
	}

	/**
	 * Sets the runningVersion.
	 * 
	 * @param runningVersion
	 *            the runningVersion to set
	 */
	public void setRunningVersion(boolean runningVersion) {
		this.runningVersion = runningVersion;
		PropertyIterator iter = propertyIterator();
		while (iter.hasNext()) {
			iter.next().setRunningVersion(runningVersion);
		}
	}

	public void recoverRunningVersion() {
		Iterator iter = propMap.entrySet().iterator();
		while (iter.hasNext()) {
			Map.Entry entry = (Map.Entry) iter.next();
			JMeterProperty prop = (JMeterProperty) entry.getValue();
			if (isTemporary(prop)) {
				iter.remove();
				clearTemporary(prop);
			} else {
				prop.recoverRunningVersion(this);
			}
		}
		emptyTemporary();
	}

	protected void emptyTemporary() {
		if (temporaryProperties != null) {
			temporaryProperties.clear();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.testelement.TestElement#isTemporary(org.apache.jmeter.testelement.property.JMeterProperty)
	 */
	public boolean isTemporary(JMeterProperty property) {
		if (temporaryProperties == null) {
			return false;
		} else {
			return temporaryProperties.contains(property);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.testelement.TestElement#setTemporary(org.apache.jmeter.testelement.property.JMeterProperty)
	 */
	public void setTemporary(JMeterProperty property) {
		if (temporaryProperties == null) {
			temporaryProperties = new LinkedHashSet();
		}
		temporaryProperties.add(property);
		if (property instanceof MultiProperty) {
			PropertyIterator iter = ((MultiProperty) property).iterator();
			while (iter.hasNext()) {
				setTemporary(iter.next());
			}
		}
	}

	/**
	 * @return Returns the threadContext.
	 */
	public JMeterContext getThreadContext() {
		if (threadContext == null) {
			/*
			 * Only samplers have the thread context set up by JMeterThread at
			 * present, so suppress the warning for now
			 */
			// log.warn("ThreadContext was not set up - should only happen in
			// JUnit testing..."
			// ,new Throwable("Debug"));
			threadContext = JMeterContextService.getContext();
		}
		return threadContext;
	}

	/**
	 * @param inthreadContext
	 *            The threadContext to set.
	 */
	public void setThreadContext(JMeterContext inthreadContext) {
		if (threadContext != null) {
			if (inthreadContext != threadContext) {
				throw new RuntimeException("Attempting to reset the thread context");
			}
		}
		this.threadContext = inthreadContext;
	}

	/**
	 * @return Returns the threadName.
	 */
	public String getThreadName() {
		return threadName;
	}

	/**
	 * @param inthreadName
	 *            The threadName to set.
	 */
	public void setThreadName(String inthreadName) {
		if (threadName != null) {
			if (!threadName.equals(inthreadName)) {
				throw new RuntimeException("Attempting to reset the thread name");
			}
		}
		this.threadName = inthreadName;
	}

	public AbstractTestElement() {
		super();
	}

	// Default implementation
	public boolean canRemove() {
		return true;
	}

	// Moved from JMeter class
	public boolean isEnabled() {
		return getProperty(TestElement.ENABLED) instanceof NullProperty || getPropertyAsBoolean(TestElement.ENABLED);
	}
}
